﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;

namespace Wyse.Server.Config
{
    /// <summary>
    /// Represents a file storing a set of properties (key-value pairs)
    /// </summary>
    public class PropertyFile
    {
        /// <summary>
        /// Creates a new, empty property file
        /// </summary>
        public PropertyFile()
        {
        }

        /// <summary>
        /// Loads a property file from a string
        /// </summary>
        /// <param name="file"></param>
        public PropertyFile(String file)
        {
            using (var s = File.OpenRead(file))
            {
                Load(s);
            }
        }

        /// <summary>
        /// Loads a property file from a stream
        /// </summary>
        /// <param name="s"></param>
        public PropertyFile(Stream s)
        {
            Load(s);
        }

        /// <summary>
        /// Loads the file from a stream
        /// </summary>
        /// <param name="s"></param>
        private void Load(Stream s)
        {
            StreamReader r = new StreamReader(s);
            String l;
            char[] trimChars = new[] { ' ', '\t' };

            while ((l = r.ReadLine()) != null)
            {
                l = l.TrimStart(trimChars);
                // skip empty lines and comments
                if (l.Length == 0 || l[0] == '#')
                    continue;

                // locate the end of the key
                int p = l.IndexOf('=');
                if (p <= 0)
                    continue;

                String key = l.Substring(0, p);
                String value = l.Substring(p + 1);

                AddValue(key, value);
            }
        }

        /// <summary>
        /// Saves the set to the specified stream
        /// </summary>
        /// <param name="stream"></param>
        public void Save(Stream stream)
        {
            StreamWriter w = new StreamWriter(stream);

            foreach (var e in mProps)
            {
                foreach (var p in e.Value.GetValueSet())
                {
                    w.Write(e.Key);
                    w.Write('=');
                    w.WriteLine(p);
                }
            }

            w.Flush();
        }

        /// <summary>
        /// Saves the set to the specified file
        /// </summary>
        /// <param name="filename"></param>
        public void Save(String filename)
        {
            using (var s = File.OpenWrite(filename))
            {
                Save(s);
            }
        }

        /// <summary>
        /// Gets or sets the property file from which
        /// default values will be obtained
        /// </summary>
        public PropertyFile Defaults
        {
            get { return mDefaults; }
            set
            {
                if (IsSealed)
                    throw new InvalidCastException(SealedExceptionMessage);
                mDefaults = value;
            }
        }

        /// <summary>
        /// Gets a value indicating that the set is empty
        /// </summary>
        public bool IsEmpty
        {
            get { return mProps.Count == 0; }
        }

        /// <summary>
        /// Gets a value indicating if the property set is sealed (read-only)
        /// </summary>
        public bool IsSealed
        {
            get;
            private set;
        }

        /// <summary>
        /// Seals the property set - after sealing it will be read-only
        /// </summary>
        public void Seal()
        {
            IsSealed = true;
        }

        /// <summary>
        /// Adds the properties from another set to the current
        /// </summary>
        /// <param name="other"></param>
        public void Merge(PropertyFile other)
        {
            foreach (var e in other.mProps)
            {
                foreach (var p in e.Value.GetValueSet())
                    AddValue(e.Key, p);
            }
        }

        /// <summary>
        /// Returns a single value as string, or the default value
        /// if the property does not exist
        /// </summary>
        /// <param name="name"></param>
        /// <param name="defVal"></param>
        /// <returns></returns>
        public String GetValue(String name, String defVal = null)
        {
            Entry e;
            if (mProps.TryGetValue(name, out e))
                return e.GetSingleValue();

            if (mDefaults != null)
                return mDefaults.GetValue(name, defVal);
            return defVal;
        }

        /// <summary>
        /// Returns a single value converted to the specified type,
        /// or the default value if the property does not exist
        /// </summary>
        /// <param name="name"></param>
        /// <param name="defVal"></param>
        /// <returns></returns>
        public TVal GetValue<TVal>(String name, TVal defVal = default(TVal))
        {
            Entry e;
            if (mProps.TryGetValue(name, out e))
                return (TVal)Convert.ChangeType(e.GetSingleValue(), typeof(TVal));

            if (mDefaults != null)
                return mDefaults.GetValue<TVal>(name, defVal);
            return defVal;
        }

        /// <summary>
        /// Returns a IEnumerable giving access to string values
        /// of a set of properties with the same key
        /// </summary>
        /// <param name="name"></param>
        /// <param name="defVal"></param>
        /// <returns></returns>
        public IEnumerable<String> GetValues(String name)
        {
            Entry e;
            if (mProps.TryGetValue(name, out e))
                return e.GetValueSet();

            if (mDefaults != null)
                return mDefaults.GetValues(name);
            return sEmptySet;
        }

        /// <summary>
        /// Returns a IEnumerable giving access to values
        /// of a set of properties with the same key converted to the specified type
        /// </summary>
        /// <param name="name"></param>
        /// <param name="defVal"></param>
        /// <returns></returns>
        public IEnumerable<TVal> GetValues<TVal>(String name)
        {
            Entry e;
            if (mProps.TryGetValue(name, out e))
                return GetConvertedValueSet<TVal>(e);

            if (mDefaults != null)
                return mDefaults.GetValues<TVal>(name);
            return new TVal[0];
        }

        /// <summary>
        /// Sets a value. If a value with the specified key
        /// already exists - overwrites it
        /// </summary>
        /// <typeparam name="TVal"></typeparam>
        /// <param name="name"></param>
        /// <param name="val"></param>
        public void SetValue<TVal>(String name, TVal val)
        {
            if (IsSealed)
                throw new InvalidOperationException(SealedExceptionMessage);
            mProps[name] = new Entry() { Value = val.ToString() };
        }

        /// <summary>
        /// Sets a value. If a value with the specified key
        /// already exists - adds it to the list
        /// </summary>
        /// <typeparam name="TVal"></typeparam>
        /// <param name="name"></param>
        /// <param name="val"></param>
        public void AddValue<TVal>(String name, TVal val)
        {
            if (IsSealed)
                throw new InvalidOperationException(SealedExceptionMessage);

            Entry e;
            if (mProps.TryGetValue(name, out e))
                e.AddValue(val.ToString());
            else
                e = new Entry() { Value = val.ToString() };
            mProps[name] = e;
        }

        private static IEnumerable<TVal> GetConvertedValueSet<TVal>(Entry e)
        {
            foreach (var v in e.GetValueSet())
                yield return (TVal)Convert.ChangeType(v, typeof(TVal));
        }

        private struct Entry
        {
            public String Value;
            public List<String> ValueList;

            public void AddValue(String val)
            {
                if (ValueList != null)
                    ValueList.Add(val);
                else
                {
                    ValueList = new List<string>(2);
                    ValueList.Add(Value);
                    ValueList.Add(val);
                    Value = null;
                }
            }

            public String GetSingleValue()
            {
                if (ValueList != null)
                    return ValueList[0];
                return Value;
            }

            public IEnumerable<String> GetValueSet()
            {
                if (ValueList != null)
                    return ValueList;
                return new[] { Value };
            }
        }

        private const String SealedExceptionMessage = "Cannot modify a sealed property set";

        private PropertyFile mDefaults;
        private Dictionary<String, Entry> mProps = new Dictionary<string, Entry>();

        private static String[] sEmptySet = new String[0];
    }
}
